[TDR Generic表][Go SDK]TDR Blob内嵌Protobuf部分字段读写
字段路径的完整语法、读写语义和限制见《TDR Blob 内嵌 Protobuf 部分字段读写》。本文介绍 Go SDK 接口与接入示例。
1. 接口说明
TDR 表的 Blob 字段(char/Byte 数组 + refer 长度字段)里如果保存的是 Protobuf 序列化数据,
可以只读取或只更新 Blob 中指定的 PB 字段,而不必整段回读、改完再整段回写。
三个接口对应三个命令字:
| 接口 | 命令字 | 值 | 说明 |
|---|---|---|---|
DoPBFieldGet |
TcaplusApiPBFieldGetReq |
0x0067 | 读取 Blob 中指定的 PB 字段 |
DoPBFieldUpdate |
TcaplusApiPBFieldUpdateReq |
0x0069 | 更新 Blob 中指定的 PB 字段,记录不存在会报错,不会自动插入 |
DoPBBatchFieldGet |
TcaplusApiPBBatchFieldGetReq |
0x0075 | 多个主键共用一组字段路径批量读取 |
Blob 中 Protobuf 字段的自增(TcaplusApiPBFieldIncreaseReq)在 TDR 表上不支持。
2. 版本要求
- Go SDK 的
v0.6.36已包含本功能;使用其他版本时,请确认提供本文所述接口。 - 服务端需要支持该特性。未适配的服务端在这三个命令上会直接返回找不到 PB 描述的错误,使用前请先确认服务端版本。
3. 准备工作
参见准备工作文档,完成使用该接口前的准备工作,并创建TDR Generic表。 service_info表service_info.xml
本特性要求表里有一个 Blob 字段 + 它的 refer 长度字段,service_info 表中对应的是:
<entry name="routeinfo_len" type="uint" defaultvalue="0" desc="路由规则信息长度" />
<entry name="routeinfo" type="char" count="1024" refer="routeinfo_len" desc="路由规则信息" />
routeinfo 是保存 PB 的 Blob 字段,routeinfo_len 是它的 refer 长度字段,
这两个字段在下面的示例里分别对应 Go 结构体的 Routeinfo 和 Routeinfo_Len。
Blob 里的 PB 定义示例(RouteInfo,不是 Tcaplus PB 表,只是普通 proto,不需要 tcaplus 的 proto 选项):
syntax = "proto3";
package routeinfo;
message RouteInfo {
uint32 version = 1;
string strategy = 2;
Weight weight = 3;
repeated string tags = 4;
repeated Instance instances = 5;
map<int64, Instance> instance_map = 6;
map<string, string> settings = 7;
}
message Weight { uint32 cpu = 1; uint32 memory = 2; }
message Instance { string addr = 1; uint32 port = 2; uint32 weight = 3; }
该 PB 定义与生成代码见示例目录。
4. 字段路径
4.1 按 PB 字段名构造路径
用 tdrpb.BuildPaths 描述要操作的字段。Blob 接收 TDR Blob 前缀、PB 类型和相对 PB 根 message 的字段名路径,SDK 完成 name path 到 tagid path 的转换,无需业务自行封装。
paths, err := tdrpb.BuildPaths(
tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil), "version", "strategy", "weight.cpu"),
tdrpb.Raw("filterdata"), // 原生 TDR 一级字段
)
if err != nil {
return
}
opt := &option.TDROpt{FieldNames: paths}
Blob 中的 PB 参数只用于获取类型,不提供更新值。更新数据由增量 PB 提供,见下一节。删除 map 元素时使用 tdrpb.Pop("routeinfo", (*routeinfo.RouteInfo)(nil), "instance_map[1001]"),生成 POP routeinfo.6[1001];不通过 opt.Operation 传递。
FieldNames 中无需手动加入 routeinfo、routeinfo_len:SDK 自动补齐一级字段及所需 refer。一次请求也可以通过多个 Blob 分组选择不同 Blob 中的字段。
4.2 BuildPaths 与 SetFieldNames
SetFieldNames 原来用于选择 TDR 一级字段,本功能将其扩展到 Blob 内嵌的 PB 字段,因此可以在同一列表中选择 PB 字段和原生 TDR 一级 value 字段。嵌套 PB 路径须使用本文的 DoPB* 接口,普通 TDR Get/Update 的字段选择规则不变。
BuildPaths 为这一步提供按字段名构造路径的封装:先将 PB 字段名转换为字段编号,再将结果赋给 option.TDROpt.FieldNames,由 DoPB* 接口构造请求并调用 SetFieldNames。对应路径如下:
| PB 定义 | name path | 完整 tagid path |
|---|---|---|
uint32 version = 1 |
version |
routeinfo.1 |
Weight weight = 3 |
weight.cpu |
routeinfo.3.1 |
repeated Instance instances = 5 |
instances[0] |
routeinfo.5#[0] |
map<int64, Instance> instance_map = 6 |
instance_map[1001] |
routeinfo.6[1001] |
| 同上 | instance_map[1001].addr |
routeinfo.6[1001].1 |
map<string, string> settings = 7 |
settings['region'] |
routeinfo.7['region'] |
需要直接指定字段编号时,可以填写 []string{"routeinfo.1", "routeinfo.2", "filterdata"},或通过 tdrpb.Raw 原样传入完整 tagid path。SetFieldNames 不接收 routeinfo.version 这样的 PB 字段名路径。
服务端没有业务 proto,tagid path 中用 [key] 区分 map、#[index] 区分 repeated;字段名接口根据 PB 类型完成这一转换。一条 PB 路径最多访问一层容器元素;packed 数组只能整组读写,不支持业务元素下标。路径冲突、类型限制及 repeated 范围读取的完整规则见《TDR Blob 内嵌 Protobuf 部分字段读写》。
5. 示例代码
示例代码见示例目录,
本特性相关的示例文件为 pbfieldprepare.go、pbfieldget.go、pbfieldupdate.go、pbbatchfieldget.go,
入口在 main.go 中。
Blob 里必须是合法的 PB 编码,普通写入的裸字符串服务端解析不了。先整段写一份完整 PB(对应示例文件的 pbFieldPrepareExample):
full := &routeinfo.RouteInfo{Version: 1, Strategy: "round_robin", /* ... */}
// 增量/整段 PB 都用 partial 语义序列化,proto2 缺 required 字段时也能正常处理
buf, err := tdrpb.MarshalPartial(full)
if err != nil {
return
}
data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"
data.Routeinfo = buf
data.Routeinfo_Len = uint32(len(buf))
// 用 Replace 而不是 Insert,示例可以反复跑
if err = client.DoReplace(TableName, data, nil); err != nil {
return
}
5.1 读取部分字段
对应示例文件的 pbFieldGetExample、pbFieldGetContainerExample。
data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"
opt := &option.TDROpt{}
if opt.FieldNames, err = tdrpb.BuildPaths(
tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil), "version", "strategy", "weight.cpu"),
tdrpb.Raw("filterdata"),
); err != nil {
return
}
if err = client.DoPBFieldGet(TableName, data, opt); err != nil {
return
}
// Routeinfo_Len 是本次部分 PB 的长度,不是记录里完整 Blob 的长度
partial := &routeinfo.RouteInfo{}
if err = tdrpb.UnmarshalPartial(data.Routeinfo, data.Routeinfo_Len, partial); err != nil {
return
}
fmt.Println(partial.GetVersion(), partial.GetStrategy(), partial.GetWeight().GetCpu(), data.Filterdata)
只有请求过的字段有值,未请求的字段是未设置状态,返回的部分记录不能当完整记录用。
5.2 更新部分字段
对应示例文件的 pbFieldUpdateExample(部分更新)、pbFieldUpdateMapExample(map 元素覆盖与删除)。
delta := &routeinfo.RouteInfo{Version: 3, Strategy: "weighted_round_robin"}
data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = "com"
data.Routeinfo = make([]byte, 1024) // Init 不会给 Blob 分配空间,tdr_count 是 1024
n, err := tdrpb.MarshalPartialTo(data.Routeinfo, delta)
if err != nil {
return
}
data.Routeinfo_Len = uint32(n)
opt := &option.TDROpt{}
if opt.FieldNames, err = tdrpb.BuildPaths(
tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil), "version", "strategy"),
); err != nil {
return
}
// 增量只带本次要写的字段,Blob 里其余字段服务端原样保留
if err = client.DoPBFieldUpdate(TableName, data, opt); err != nil {
return
}
TDR 的 tinyint 数组会被生成为 []int8,这种表换用 tdrpb.MarshalPartialToInt8 /
tdrpb.UnmarshalPartialInt8,参数含义一致。
5.3 批量读取
对应示例文件的 pbBatchFieldGetExample。
var dataSlice []record.TdrTableSt
for i := 0; i < 10; i++ {
data := service_info.NewService_Info()
data.Gameid = "dev"
data.Envdata = "oa"
data.Name = fmt.Sprintf("%d", i)
dataSlice = append(dataSlice, data)
}
// 字段路径对所有主键生效,单条记录的结果看 opt.BatchResult,版本看 opt.BatchVersion
opt := &option.TDROpt{}
if opt.FieldNames, err = tdrpb.BuildPaths(
tdrpb.Blob("routeinfo", (*routeinfo.RouteInfo)(nil), "version", "strategy"),
tdrpb.Raw("filterdata"),
); err != nil {
return
}
// 请求级成功不等于每条记录都成功,需要逐条检查 opt.BatchResult
if err = client.DoPBBatchFieldGet(TableName, dataSlice, opt); err != nil {
fmt.Printf("batch field get: %s\n", err)
// 可能只有部分记录失败,继续检查已经返回的逐条结果。
}
if len(opt.BatchResult) != len(dataSlice) {
return // 请求构造失败等情况下,逐条结果尚未建立。
}
for i, data := range dataSlice {
if opt.BatchResult[i] != nil {
fmt.Printf("record %d failed: %s\n", i, opt.BatchResult[i].Error())
continue
}
row := data.(*service_info.Service_Info)
partial := &routeinfo.RouteInfo{}
if err = tdrpb.UnmarshalPartial(row.Routeinfo, row.Routeinfo_Len, partial); err != nil {
continue
}
fmt.Printf("record %d version %d, filterdata %s\n", i, opt.BatchVersion[i], row.Filterdata)
}
单次最多 1024 条主键,响应不保证顺序,按主键回填到 dataSlice 的对应下标。
分包由服务端通过响应里的 LeftNum 驱动,SDK 已经收完所有分包,不需要设置 MultiFlag。
6. option 支持情况
opt 必填且 opt.FieldNames 不能为空。不支持的选项会返回错误;批量 Condition 返回 GEN_ERR_INVALID_ARGUMENTS。
| option | FieldGet | FieldUpdate | BatchFieldGet |
|---|---|---|---|
FieldNames |
必填 | 必填 | 必填 |
Condition |
支持 | 支持 | 不支持(请求体没有条件字段) |
Timeout / Ctx / Flags / UserBuff |
支持 | 支持 | 支持 |
Version |
支持,入参兼出参 | 支持 | 拒绝,版本只从 BatchVersion 出参 |
VersionPolicy |
拒绝 | 支持 | 拒绝 |
ResultFlagForSuccess / ResultFlagForFail |
拒绝 | 支持 | 拒绝 |
BatchResult / BatchVersion |
不适用 | 不适用 | 支持 |
MultiFlag / Operation / ResultFlag / IncField / TTL / Limit / Offset / ExpireTime 等 |
拒绝 | 拒绝 | 拒绝 |
Version 在 DoPBFieldGet 上既是入参也是出参,响应回来时被改写为记录的当前版本。
7. 错误码
本地检查(接口返回值):
| 场景 | 错误码 |
|---|---|
opt 为 nil、FieldNames 为空、使用了不支持的 option |
ParameterInvalid |
| 路径为空或超过 1023 字节 | API_ERR_OVER_MAX_FIELD_NAME_LEN |
| 补齐后字段数超过 256 | API_ERR_OVER_MAX_VALUE_FIELD_NUM |
路径重复或父子路径冲突、未先 SetData |
API_ERR_PARAMETER_INVALID |
| 路径的一级字段不是本表的 value 字段 | API_ERR_FIELD_NOT_EXSIST |
| PB 字段不存在 | API_ERR_FIELD_NOT_EXSIST |
| map / repeated / 嵌套 message 类型不匹配 | API_ERR_FIELD_TYPE_NOT_MATCH |
| Blob 容量不足 | API_ERR_OVER_MAX_FIELD_VALUE_LEN |
发送后的结果看接口返回值;批量还要看每条 opt.BatchResult。常见服务端错误码:
| 错误码 | 含义 |
|---|---|
TXHDB_ERR_RECORD_NOT_EXIST |
记录不存在 |
COMMON_ERR_ELEMENT_NOT_EXIST |
指定的 map key 或 repeated 下标不存在 |
COMMON_ERR_CONDITION_NOT_MATCHED |
条件不成立 |
SVR_ERR_FAIL_INVALID_VERSION |
版本校验失败 |
详见错误码含义和处理方法。
8. 注意事项
- 只支持 Generic 表,List 表只能由服务端拒绝,SDK 本地拿不到表类型。
- Blob 里必须是当前业务 descriptor 对应的 PB 编码,历史数据的 schema 兼容性由业务保证。
- 返回的
routeinfo_len是本次部分 PB 的长度,不是记录中完整 Blob 的长度; 解析出的 PB 对象只包含本次请求的字段,不能直接用于整段 Blob 覆盖。 - SDK 不做 PB 编解码,增量 PB 的构造、Blob 的写入、响应 Blob 的解析都由业务负责,
tdrpb只提供MarshalPartial/MarshalPartialTo/UnmarshalPartial等便利函数。 - 路径里的一级字段名取 TDR 表定义中的字段名(
tdr_field),不是 Go 结构体字段名。 - 字段名使用
.proto中的原始 field name,不使用 JSON name。 - 嵌套结构体里的 Blob(如
extra.bin)也支持,SDK 自动补齐其所属的一级结构体字段。